/*
 * #!
 * Ontopia Vizigator
 * #-
 * Copyright (C) 2001 - 2013 The Ontopia Project
 * #-
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * !#
 */

package net.ontopia.topicmaps.viz;

import com.touchgraph.graphlayout.Node;
import java.awt.Color;
import java.awt.Font;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.UIManager;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.infoset.impl.basic.URILocator;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.AssociationRoleIF;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.utils.OntopiaRuntimeException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

/**
 * INTERNAL: Stores and manages configuration. The configuration information is
 * stored as a topic map.
 */
public class VizTopicMapConfigurationManager extends VizConfigurationManager {
  private VizTopicTypePriorityConfigManager priorityManager;

  private static final String TYPE_COLOR = BASE + "type-color";
  private static final String TOPIC_TYPE_SHAPE = BASE + "topic-type-shape";
  private static final String OVERRIDE_RANDOM_COLORS
      = BASE + "override-random-colors";

  private static final String ASSOC_TYPE_SHAPE = BASE + "assoc-type-shape";
  private static final String ASSOCIATION_TYPE_COLOR_AUTOGENERATED
  = BASE + "association-type-color-autogenerated";
  private static final String TOPIC_TYPE_COLOR_AUTOGENERATED
  = BASE + "topic-type-color-autogenerated";
  private static final String TYPE_VISIBLE = BASE + "type-visible";
  private static final String SHOW_ROLE_HOVER_HELP = BASE
      + "show-role-hover-help";
  private static final String ENABLE_MOTION_KILLER = BASE
  + "enable-motion-killer";
  private static final String TYPE_ICON_FILENAME = BASE + "type-icon-filename";
  private static final String TYPE_LINE_WEIGHT = BASE + "type-line-weight";
  private static final String TYPE_SHAPE_PADDING = BASE + "type-shape-padding";
  private static final String TYPE_ICON = BASE + "type-icon";
  private static final String TYPE_FONT = BASE + "type-font";
  private static final String SINGLE_MOUSE_CLICK = BASE + "single-mouse-click";
  private static final String LOCALITY_ALGORITHM = BASE + "locality-algorithm";
  private static final String MOTION_KILLER_DELAY = BASE +
      "motion-killer-delay";
  private static final String MAX_TOPIC_NAME_LENGTH = BASE +
  "max-topic-name-length";
  private static final String DOUBLE_MOUSE_CLICK = BASE + "double-mouse-click";

  private static final String START_TOPIC = BASE + "start-topic";
  private static final String SUBJECT_INDICATOR = BASE + "subject-indicator";
  private static final String SUBJECT = BASE + "subject";
  private static final String SOURCE_LOCATOR = BASE + "source-locator";

  private static final String TYPE_INSTANCE_TYPE = BASE + "type-instance";
  private boolean visibleByDefault = true;
  private final Font defaultFont = UIManager.getFont("TextArea.font");
  private final Font defaultAssociationFont = new Font(defaultFont.getFamily(),
      defaultFont.getStyle(), defaultFont.getSize() - 2);

  private TopicIF typeIcon;

  // Topics representing configuration properties of topic and association types
  private TopicIF typeColor; // Represents the color.
  private TopicIF typeVisible; // Represents whether it should be visible.
  private TopicIF typeFont; // Represents the font.
  private TopicIF typeExcluded; // Represents whether it should be excluded.

  // Topics representing configuration properties of topic types.
  private TopicIF typeIconFilename; // Represents the icon.
  private TopicIF topicTypeShape; // Represents the shape.
  private TopicIF typeShapePadding; // Represents the shape padding.
  private TopicIF topicTypeColorAutogeneratedTopic; 
    // Represents the shape padding.

  // Topics representing configuration properties of association types.
  private TopicIF assocTypeShape; // Represents the shape.
  private TopicIF typeLineWeight; // Represents the line weight.
  private TopicIF associationTypeColorAutogeneratedTopic;
    // Represents the shape padding.

  private TopicIF showRoleHoverHelp;

  private TopicIF enableMotionKiller;

  private TopicIF startTopic;
  private TopicIF typeInstanceType;
  private Map iconCache;
  private Map fontCache;
  private Map colourCache;
  private ColorAssigner associationTypeColorAssigner;
  private ColorAssigner topicTypeColorAssigner;

  public static final Color DEFAULT_PANEL_BACKGROUND_COLOUR = new Color(240,
      240, 240);

  private TopicIF doubleMouseClick;
  private TopicIF singleMouseClick;
  private TopicIF localityAlgorithm;
  private TopicIF motionKillerDelay;
  private TopicIF maxTopicNameLength;

  public static final int NODE_ORIENTED = 0;
  public static final int EDGE_ORIENTED = 1;

  public static final int EXPAND_NODE = 0;
  public static final int SET_FOCUS_NODE = 1;
  public static final int GO_TO_TOPIC = 2;
  private static final int DEFAULT_SINGLE_CLICK = EXPAND_NODE;
  private static final int DEFAULT_LOCALITY_ALGORITHM = NODE_ORIENTED;
  private static final int DEFAULT_KILLER_DELAY = 3;
  public static final int DEFAULT_MAX_TOPIC_NAME_LENGTH = 15;
  private static final int DEFAULT_DOUBLE_CLICK = SET_FOCUS_NODE;

  private TopicIF displayScopedAssociationNames;

  private static final String DISPLAY_SCOPED_ASSOC_NAMES = BASE
      + "display-scoped-assoc-names";

  private TopicIF scopeFilter;

  private TopicIF filterStrictness;
  public static final int SHOW_ALL_ASSOCIATION_SCOPES = 0;
  public static final int LOOSE_ASSOCIATION_SCOPES = 1;
  public static final int STRICT_ASSOCIATION_SCOPES = 2;

  private static final String TYPE_EXCLUDED = BASE + "type-excluded";

  private static final String SCOPE_FILTER = BASE + "scope-filter";

  private static final String FILTER_STRICTNESS = BASE + "filter-strictness";

  private static final int DEFAULT_FILTER_SELECTION = 
      VizTopicMapConfigurationManager.FILTER_IN;

  private TopicIF subjectIndicator;

  private TopicIF subject;

  private TopicIF sourceLocator;
  protected static final String NULL_TOPIC = BASE + "null";
  protected TopicIF nullTopic;
  private TopicIF scopingTopic;
  private TopicIF overrideRandomColorsTopic;

  public static final int FILTER_DEFAULT = 2;
  public static final int FILTER_OUT = 1;
  public static final int FILTER_IN = 0;
  
  private static final String SCOPING_TOPIC = BASE + "scoping-topic";

  /**
   * Constructor initializes the configuration by loading a topic map from the
   * URL given in the parameter.
   */
  public VizTopicMapConfigurationManager(File tmfile) throws IOException {
    super(tmfile);
    setupPriorityManager();
  }
  
  /**
   * Constructor initializes the configuration by loading a topic map from the
   * URL given in the parameter.
   */
  public VizTopicMapConfigurationManager(URL tmurl) throws IOException {
    super(tmurl);
    setupPriorityManager();
  }

  /**
   * Creates an empty configuration manager where everything is set to default.
   */
  public VizTopicMapConfigurationManager() {
    super();
    setupPriorityManager();
  }

  @Override
  protected void init() {
    super.init();

    startTopic = getTopic(START_TOPIC);
    scopingTopic = getTopic(SCOPING_TOPIC);

    subjectIndicator = getTopic(SUBJECT_INDICATOR);
    subject = getTopic(SUBJECT);
    sourceLocator = getTopic(SOURCE_LOCATOR);

    typeVisible = getTopic(TYPE_VISIBLE);
    typeColor = getTopic(TYPE_COLOR);
    topicTypeShape = getTopic(TOPIC_TYPE_SHAPE);
    overrideRandomColorsTopic = getTopic(OVERRIDE_RANDOM_COLORS);
    assocTypeShape = getTopic(ASSOC_TYPE_SHAPE);
    typeIconFilename = getTopic(TYPE_ICON_FILENAME);
    typeIcon = getTopic(TYPE_ICON);
    typeLineWeight = getTopic(TYPE_LINE_WEIGHT);
    typeFont = getTopic(TYPE_FONT);
    typeShapePadding = getTopic(TYPE_SHAPE_PADDING);
    associationTypeColorAutogeneratedTopic = 
      getTopic(ASSOCIATION_TYPE_COLOR_AUTOGENERATED);
    topicTypeColorAutogeneratedTopic =
      getTopic(TOPIC_TYPE_COLOR_AUTOGENERATED);
    typeExcluded = getTopic(TYPE_EXCLUDED);
    scopeFilter = getTopic(SCOPE_FILTER);
    filterStrictness = getTopic(FILTER_STRICTNESS);

    singleMouseClick = getTopic(SINGLE_MOUSE_CLICK);
    doubleMouseClick = getTopic(DOUBLE_MOUSE_CLICK);
    localityAlgorithm = getTopic(LOCALITY_ALGORITHM);
    motionKillerDelay = getTopic(MOTION_KILLER_DELAY);
    maxTopicNameLength = getTopic(MAX_TOPIC_NAME_LENGTH);
    
    typeInstanceType = getTopic(TYPE_INSTANCE_TYPE, Messages
        .getString("Viz.InstanceOf"));
    showRoleHoverHelp = getTopic(SHOW_ROLE_HOVER_HELP);
    enableMotionKiller = getTopic(ENABLE_MOTION_KILLER);
    displayScopedAssociationNames = getTopic(DISPLAY_SCOPED_ASSOC_NAMES);

    Node.DEFAULT_TYPE = Node.TYPE_ROUNDRECT;

    associationTypeColorAssigner = new ColorAssigner();
    topicTypeColorAssigner = new ColorAssigner();

    colourCache = new HashMap();
    iconCache = new HashMap();
    fontCache = new HashMap();
  }

  public TopicIF getOverrideColorsTopic() {
    return overrideRandomColorsTopic;
  }
  public TopicIF getTopicTypeShapeTopic() {
    return topicTypeShape;
  }
  
  public TopicIF getAssociationTypeColorAutogeneratedTopic() {
    return associationTypeColorAutogeneratedTopic;
  }

  public TopicIF getTopicTypeColorAutogeneratedTopic() {
    return topicTypeColorAutogeneratedTopic;
  }

  public TopicIF getAssociationTypeShapeTopic() {
    return assocTypeShape;
  }
  
  public TopicIF getTopicTypeShapePaddingTopic() {
    return typeShapePadding;
  }
  
  public TopicIF getTopicTypeIconTopic() {
    return typeIcon;
  }
  
  public TopicIF getTopicTypeFontTopic() {
    return typeFont;
  }
  
  public TopicIF getTopicTypeColorTopic() {
    return typeColor;
  }
  
  public TopicIF getAssociationTypeColorTopic() {
    return typeColor;
  }
  
  // FIXME: Using the same type for topic types and association types is risky,
  //   as a topic may be both topic type and association type at the same time.
  public TopicIF getTypeVisibleTopic() {
    return typeVisible;
  }
  
  public TopicIF getTopicTypeExcludedTopic() {
    return typeExcluded;
  }
  
  public TopicIF getAssociationTypeLineWeightTopic() {
    return typeLineWeight;
  }
  
  public TopicIF getAssociationTypeFontTopic() {
    return typeFont;
  }
  
  private void setupPriorityManager() {
    priorityManager = new VizTopicTypePriorityConfigManager(this);
  }
  
  public VizTopicTypePriorityConfigManager getTTPriorityManager() {
    return priorityManager;
  }

  // --- External interface

  public Color getAssociationTypeColor(TopicIF type) {
    Color c = (Color) colourCache.get(type);

    if (getUsesDefault(type, false) && defaultOverrides(false))
      c = lookupColor(defaultAssociationType, typeColor);
    if (c == null)
      c = lookupColor(type, typeColor);
    if (c == null && defaultOverrides(false))
      c = lookupColor(defaultAssociationType, typeColor);
    if (c == null) {
      c = associationTypeColorAssigner.getNextColor();
      setTypeColor(type, c);
      setOccurenceValue(type, getAssociationTypeColorAutogeneratedTopic(),
                        true);
    }
    return c;
  }

  /**
   * returns true iff the default type is set to verride autogenerated values.
   * @param isTopicType Set to true to check for the association default type,
   * and false for the topic default type.
   */
  public boolean defaultOverrides(boolean isTopicType) {
    return getOccurrenceValue(isTopicType ? defaultType 
        : defaultAssociationType, getOverrideColorsTopic(), false);
  }
  
  /**
   * returns true iff the given topic type is set to use default when it doesn't
   * its own colour.
   * @param type The type to check.
   * @param isTopicType 
   */
  public boolean getUsesDefault(TopicIF type, boolean isTopicType) {
    // Types that don't specify whether they use the dafault state do not by
    // default.
    return getOccurrenceValue(type, isTopicType
        ? getTopicTypeColorAutogeneratedTopic()
        : getAssociationTypeColorAutogeneratedTopic(), false);
  }
  
  public Color getTopicTypeColor(TopicIF type) {
    Color c = (Color) colourCache.get(type);

    if (getUsesDefault(type, true) && defaultOverrides(true))
      c = lookupColor(defaultType, typeColor);
    if (c == null)
      c = lookupColor(type, typeColor);
    if (c == null && defaultOverrides(true))
      c = lookupColor(defaultType, typeColor);
    if (c == null) {
      c = topicTypeColorAssigner.getNextColor();
      setTypeColor(type, c);
      setOccurenceValue(type, getTopicTypeColorAutogeneratedTopic(), true);
    }
    return c;
  }

  public int getTopicTypeShape(TopicIF topictype) {
    return getOccurrenceValue(topictype, topicTypeShape,
        getOccurrenceValue(defaultType, topicTypeShape, Node.DEFAULT_TYPE));
  }

  public boolean hasOccurrence(TopicIF topictype, TopicIF type) {
    TopicIF cfgtopic = getConfigTopic(topictype);
    return cfgtopic != null && getOccurrence(cfgtopic, type) != null;
  }

  public Font getDefaultFont() {
    return defaultFont;
  }

  public void setTypeColor(TopicIF type, Color c) {
    setColor(type, typeColor, c);
    colourCache.put(type, c);
  }

  //-----------------------------------------------
  
  /**
   * Sets the colour of a given 'type' and updates 'view' accordingly.
   * If the type is defaultType/defaultTopicType, all topic/association types
   * that have no explicit colour setting are updated with this colour. 
   */
  public void setTypeColor(TopicIF type, Color c, TopicMapView view) {
    setTypeColor(type, c);
    setOccurenceValue(type,
        getTopicTypeColorAutogeneratedTopic(), false);
    setOccurenceValue(type,
        getAssociationTypeColorAutogeneratedTopic(), false);

    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
        boolean hasColor = hasOccurrence(currentType,
            getTopicTypeColorTopic());
        boolean isAutogenerated = getOccurrenceValue(currentType,
            getTopicTypeColorAutogeneratedTopic(), false); 
        boolean defaultOverrules = getOccurrenceValue(type,
            getOverrideColorsTopic(), false);
            
        // If the type doesn't have an explicicly defined color yet:
        if (!hasColor || isAutogenerated && defaultOverrules) {
          // Update it in view with the new default color.
          if (currentType == null) // Untyped topic
            removeOccurrence(untypedTopic,
                getTopicTypeColorTopic());
          else 
            removeOccurrence(currentType, 
                getTopicTypeColorTopic());
          view.setTypeColor(currentType, c);
        }
      }
    } else if (type == defaultAssociationType) {
      // For each association type
      Iterator typesIt = view.getAssociationTypes().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
        boolean hasColor = hasOccurrence(currentType,
            getAssociationTypeColorTopic());
        boolean isAutogenerated = getOccurrenceValue(currentType,
            getAssociationTypeColorAutogeneratedTopic(), false); 
        boolean defaultOverrules = getOccurrenceValue(type,
            getOverrideColorsTopic(), false);

        // If the type doesn't have a defined color yet:
        if (!hasColor || isAutogenerated && defaultOverrules) {
          // Update it in view with the new default color.
          removeOccurrence(currentType, 
              getAssociationTypeColorTopic());
          view.setTypeColor(currentType, c);
        }
      }
    } else
      view.setTypeColor(type, c);
  }

  /**
   * Sets the color of a given topic/association 'type' to the default value
   * and updates 'view' accordingly. 
   */
  public void setColorToDefault(TopicIF type, boolean topicType,
      TopicMapView view) {
    removeOccurence(type, topicType ? getTopicTypeColorTopic()
        : getAssociationTypeColorTopic());
    
    if (topicType)
      view.setTypeColor(type,
          getTopicTypeColor(defaultType));
    else
      view.setTypeColor(type,
          getAssociationTypeColor(defaultAssociationType));
  }
  
  /**
   * Sets the given topic 'type' be either filtered in, filtered out or to
   * use the filter setting of the default type.
   * For defaultType, updates all types with no explicit setting.
   * Updates view accordingly.
   */
  public void setTypeVisibility(TopicIF type, int visibility,
                                TopicMapView view) {
    // Only set configuration for default type itself.
    setTypeVisible(type, visibility);

    boolean visible = 
        visibility == VizTopicMapConfigurationManager.FILTER_IN ||
                      (visibility == VizTopicMapConfigurationManager
                           .FILTER_DEFAULT &&
                       isTopicTypeVisible(defaultType));

    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        // FIXME: Added this code to avoid a NullPointerException, which should,
        // never occur, but did occur after filtering in the topic type Opera
        // in the Opera topic map and then filtering out Default type.
        // Would be good to find out why it occurred, as this is a possible
        // source of wrong filtering.
        if (currentType == null)
          continue;
        
        // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getTypeVisibleTopic())) {
          // Update it in view with the new default filter setting.
          view.setTopicTypeVisible(currentType,
                                   visibility == VizTopicMapConfigurationManager
                                       .FILTER_IN);
        }
      }
    } else {
      view.setTopicTypeVisible(type, visible);
    }
    
    // Having made the appropriate changes to the filtering configuration, now
    // add any nodes and edges within locality's reach that are filtered in.
    if (visible)
      view.loadNodesInLocality(view.getFocusNode(), true, false);
  }
  
  /**
   * Sets the given association 'type' be either filtered in, filtered out or to
   * use the filter setting of the default type.
   * For defaultAssociationType, updates all types with no explicit setting.
   * Updates view accordingly.
   */
  public void setAssociationTypeVisible(TopicIF type, int visibility, 
                                        TopicMapView view) {
    setTypeVisible(type, visibility);

    boolean visible = 
        visibility == VizTopicMapConfigurationManager.FILTER_IN ||
        (visibility == VizTopicMapConfigurationManager.FILTER_DEFAULT &&
         isTopicTypeVisible(defaultAssociationType));
    if (type == defaultAssociationType) {
      // For each association type
      Iterator typesIt = view.getAssociationTypes().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
        // If visibility hasn't been set for this type yet:
        if (!hasOccurrence(currentType,
            getTypeVisibleTopic())) {
          // Update it in view with the new default filter setting.
          view.setAssociationTypeVisible(currentType, 
                       visibility == VizTopicMapConfigurationManager.FILTER_IN);
        }
      }
    } else {
      view.setAssociationTypeVisible(type, visible);
    }

    // Having made the appropriate changes to the filtering configuration, now
    // add any nodes and edges within locality's reach that are filtered in.
    if (visible)
      view.loadNodesInLocality(view.getFocusNode(), true, false);
  }
  
  // FIXME: SHOULD BECOME REDUNDANT. THEN REMOVE.
  /**
   * Sets the given topic 'type' to be visible and updates 'view' accordingly.
   * For defaultType, updates all types with no explicit setting. 
   */
  public void setTypeVisible(TopicIF type, boolean visible, TopicMapView view) {
    // Only set configuration for default type itself.
    setTypeVisible(type, visible);

    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
        // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getTypeVisibleTopic())) {
          // Update it in view with the new default filter setting.
          view.setTopicTypeVisible(currentType, visible);
        }
      }
    } else
      view.setTopicTypeVisible(type, visible);
        
    if (visible)
      view.loadNodesInLocality(view.getFocusNode(), true, false);
  }

  // FIXME: TO BECOME REDUNDANT. THEN REMOVE.
  /**
   * Sets the given association 'type' to be visible and updates 'view'
   * accordingly. 
   * For defaultAssociationType, updates all types with no explicit setting. 
   */
  public void setAssociationTypeVisible(TopicIF type, boolean visible, 
                                        TopicMapView view) {
    setTypeVisible(type, visible);
    if (type == defaultAssociationType) {
      // For each association type
      Iterator typesIt = view.getAssociationTypes().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
        // If visibility hasn't been set for this type yet:
        if (!hasOccurrence(currentType,
            getTypeVisibleTopic())) {
          // Update it in view with the new default filter setting.
          view.setAssociationTypeVisible(currentType, visible);
        }
      }
    } else
      view.setAssociationTypeVisible(type, visible);
  }

  /**
   * Sets the shape of the given topic 'type' and updates 'view' accordingly.
   * For defaultType, updates all types with no explicit setting. 
   */
  public void setTopicTypeShape(TopicIF type, int i, TopicMapView view) {
    setTopicTypeShape(type, i);
    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getTopicTypeShapeTopic()))
          // Update it in view with the new default shape.
          view.setTopicTypeShape(currentType, i);
      }
    } else if (i == TypesConfigFrame.UNDEFINED_NODE_SHAPE) {
      removeOccurence(type, getTopicTypeShapeTopic());
      view.setTopicTypeShape(type,
          getTopicTypeShape(defaultType));
    } else
      view.setTopicTypeShape(type, i);
  }

  /**
   * Sets the shape of the given association 'type' and updates 'view'
   * accordingly.
   * For defaultType, updates all types with no explicit setting. 
   */
  public void setAssociationTypeShape(TopicIF type, int i, TopicMapView view) {
    setAssociationTypeShape(type, i);
    if (type == defaultAssociationType) {
      // For each topic type
      Iterator typesIt = view.getAssociationTypes().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getAssociationTypeShapeTopic()))
          // Update it in view with the new default shape.
          view.setAssociationTypeShape(currentType, i);
      }
    } else if (i == TypesConfigFrame.UNDEFINED_EDGE_SHAPE) {
      removeOccurence(type, getAssociationTypeShapeTopic());
      view.setAssociationTypeShape(type,
          getAssociationTypeShape(defaultAssociationType));
    } else
      view.setAssociationTypeShape(type, i);
  }

  /**
   * Sets the font of a given topic/association 'type' to the default value
   * and updates 'view' accordingly. 
   */
  public void setFontToDefault(TopicIF type, boolean topicType,
                               TopicMapView view) {
    removeOccurence(type, topicType ? getTopicTypeFontTopic()
        : getAssociationTypeFontTopic());
    
    if (topicType)
      view.setTypeFont(type,
          getTypeFont(defaultType));
    else
      view.setTypeFont(type,
          getTypeFont(defaultAssociationType));
  }

  /**
   * Sets the font of the given topic/association 'type' and updates 'view'
   * accordingly. For defaultType/defaultAssociationType, updates all types
   * with no explicit setting. 
   */
  public void setTypeFont(TopicIF type, Font font, TopicMapView view) {
    setTypeFont(type, font);
    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined font yet:
        if (!hasOccurrence(currentType,
            getTopicTypeFontTopic()))
          // Update it in view with the new default font.
          view.setTypeFont(currentType, font);
      }
    } else if (type == defaultAssociationType) {
      // For each association type
      Iterator typesIt = view.getAssociationTypes().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined font yet:
        if (!hasOccurrence(currentType,
            getAssociationTypeFontTopic()))
          // Update it in view with the new default font.
          view.setTypeFont(currentType, font);
      }
    } else
      view.setTypeFont(type, font);
  }

  /**
   * Sets the line weight of the given association 'type' and updates 'view'
   * accordingly.
   * For defaultAssociationType, updates all types with no explicit setting. 
   */
  public void setAssociationTypeLineWeight(TopicIF type, int i,
                                           TopicMapView view) {
    setTypeLineWeight(type, i);

    if (type == defaultAssociationType) {
      // For each topic type
      Iterator typesIt = view.getAssociationTypes().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
        // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getAssociationTypeLineWeightTopic()))
          // Update it in view with the new default shape.
          view.setAssociationTypeLineWeight(currentType, i);
      }
    } else if (i == TypesConfigFrame.UNDEFINED_EDGE_SHAPE_WEIGHT) {
      removeOccurence(type,
          getAssociationTypeLineWeightTopic());
      view.setAssociationTypeLineWeight(type,
          getAssociationTypeLineWeight(defaultType));
    } else
      view.setAssociationTypeLineWeight(type, i);
  }
  
  /**
   * Sets the shape padding of the given topic 'type' and updates 'view'
   * accordingly.
   * For defaultType, updates all types with no explicit setting. 
   */
  public void setTopicTypeShapePadding(TopicIF type, int i, TopicMapView view) {
    setTopicTypeShapePadding(type, i);

    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getTopicTypeShapePaddingTopic()))
          // Update it in view with the new default shape.
          view.setTopicTypeShapePadding(currentType, i);
      }
    } else if (i == TypesConfigFrame.UNDEFINED_NODE_SHAPE_PADDING) {
      removeOccurence(type, getTopicTypeShapePaddingTopic());
      view.setTopicTypeShapePadding(type,
          getTopicTypeShape(defaultType));
    } else
      view.setTopicTypeShapePadding(type, i);
  }
  
  /**
   * Sets the name of the icon file (optional) of the given topic 'type' and
   * updates 'view' accordingly.
   * For defaultType, updates all types with no explicit setting. 
   */
  public void setTypeIconFilename(TopicIF type, String string,
                                  TopicMapView view) {
    setTypeIconFilename(type, string);
    Icon icon = getTypeIcon(type);

    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getTopicTypeIconTopic()))
          // Update it in view with the new default shape.
          view.setTypeIcon(currentType, icon);
      }
    } else if (type == defaultAssociationType) {
        // For each topic type
        Iterator typesIt = view.getAssociationTypes().iterator();
        while (typesIt.hasNext()) {
          TopicIF currentType = (TopicIF)typesIt.next();
          
         // If the type doesn't have a defined shape yet:
          if (!hasOccurrence(currentType,
              getTopicTypeIconTopic()))
            // Update it in view with the new default shape.
            view.setTypeIcon(currentType, icon);
        }
    } else if (string == null) {
      removeOccurence(type, getTopicTypeIconTopic());
      view.setTypeIcon(type,
          getTypeIcon(defaultType));
    } else
      view.setTypeIcon(type, icon);
  }

  /**
   * Sets the given topic 'type' to be included and updates 'view' accordingly.
   * For defaultType, updates all types with no explicit setting. 
   */
  public void setTypeIncluded(TopicIF type, TopicMapView view) {
    setTypeIncluded(type);

    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getTopicTypeExcludedTopic()))
          // Update it in view with the new default shape.
          view.setTopicTypeExcluded(currentType, false);
      }
    } else
      view.setTopicTypeExcluded(type, false);
  }

  /**
   * Sets the given topic 'type' to be excluded and updates 'view' accordingly.
   * For defaultType, updates all types with no explicit setting. 
   */
  public void setTypeExcluded(TopicIF type, TopicMapView view) {
    setTypeExcluded(type);

    if (type == defaultType) {
      // For each topic type
      Iterator typesIt = view.getAllTopicTypesWithNull().iterator();
      while (typesIt.hasNext()) {
        TopicIF currentType = (TopicIF)typesIt.next();
        
       // If the type doesn't have a defined shape yet:
        if (!hasOccurrence(currentType,
            getTopicTypeExcludedTopic()))
          // Update it in view with the new default shape.
          view.setTopicTypeExcluded(currentType, true);
      }
    } else
      view.setTopicTypeExcluded(type, true);
  }

  // ----------------------
  
  public void setTopicTypeShape(TopicIF type, int i) {
    setOccurrenceValue(type, topicTypeShape, i);
  }

  public void setAssociationTypeShape(TopicIF type, int i) {
    setOccurrenceValue(type, assocTypeShape, i);
  }

  public void setTypeLineWeight(TopicIF type, int i) {
    setOccurrenceValue(type, typeLineWeight, i);
  }

  public void setTopicTypeShapePadding(TopicIF type, int i) {
    setOccurrenceValue(type, typeShapePadding, i);
  }

  public void setTypeVisible(TopicIF type, int visibility) {
    if (visibility == VizTopicMapConfigurationManager.FILTER_DEFAULT) {
      if (typeVisible == defaultAssociationType || typeVisible == defaultType)
        return;
      removeOccurence(type, typeVisible);
    } else
      setOccurenceValue(type, typeVisible, 
                       visibility == VizTopicMapConfigurationManager.FILTER_IN);
  }

  // FIXME: THIS METHOD SHOULD BECOME REDUNDANT. REMOVE IT THEN.
  public void setTypeVisible(TopicIF type, boolean visible) {
    setOccurenceValue(type, typeVisible, visible);
  }

  public boolean isAssociationTypeVisible(TopicIF assoctype) {
    return lookupVisible(assoctype, typeVisible, defaultAssociationType);
  }

  public boolean isTopicTypeVisible(TopicIF assoctype) {
    return lookupVisible(assoctype, typeVisible, defaultType);
  }

  public boolean lookupVisible(TopicIF type, TopicIF occtype, TopicIF defType) {
    TopicIF cfgtopic = getConfigTopic(type);
    OccurrenceIF occ = getOccurrence(cfgtopic, occtype);
    
    if (occ == null || occ.getValue() == null) {
      cfgtopic = getConfigTopic(defType);
      occ = getOccurrence(cfgtopic, occtype);
    }
      
    if (occ == null || occ.getValue() == null)
      return visibleByDefault;

    return occ.getValue().equalsIgnoreCase("true");
  }

  public boolean isVisible(TopicIF topic) {
    Iterator it = topic.getTypes().iterator();
    if (!it.hasNext()) // no types
      return isTopicTypeVisible(null);

    while (it.hasNext()) {
      TopicIF type = (TopicIF) it.next();
      if (!isTopicTypeVisible(type))
        return false;
    }
    return true;
  }

  public boolean isVisible(AssociationIF assoc) {
    // Only check the visibility of the association
    // The visibility of the roles is taken care of in #assertNode()

    return isAssociationTypeVisible(assoc.getType()) && 
        matchesFilter(assoc.getScope());
  }

  /**
   * Checks if a given scope matches the association scope filter.
   * See matchScope(scope, filter) for details.
   * @param scope The scope to check for matching.
   * @return true iff scope matches the association scope filter.
   */
  public boolean matchesFilter(Collection scope) { 
    Collection filter = getAssociationScopeFilter();
    return matchesFilter(scope, filter);
  }
  
  /**
   * Checks if a given scope (collection of topics) matches a given filter 
   *     (another collection of topics). 
   * @param scope The scope to match with the filter.
   * @param filter The filter for the scope to be matched with.
   * @return true iff scope matches filter. The scope either has to contain one,
   *   all or none of the topics in filter, 
   *   depending on getAssociationFilterTightness().
   */
  public boolean matchesFilter(Collection scope, Collection filter) {
    // An empty scope matches all filters..
    if (scope.isEmpty())
      return true;
       
    // An emtpty filter is matched by all scopes.
    if (filter.isEmpty())
      return true;
    
    int associationFilterStrictness = getAssociationScopeFilterStrictness();
    Iterator scopeIt = scope.iterator();
    switch (associationFilterStrictness) {
      case SHOW_ALL_ASSOCIATION_SCOPES:
        // No filter: Show all associations (regardless of scope).
        return true;
      case LOOSE_ASSOCIATION_SCOPES:
        // Show associations with scope matching one+ topic in the filter.
        while (scopeIt.hasNext())
          if (filter.contains(getConfigTopic((TopicIF)scopeIt.next())))
            return true;
        return false;
      case STRICT_ASSOCIATION_SCOPES:
        // Show associaitons with scope matching all topics in the filter.
        int filterMatches = 0;
        while (scopeIt.hasNext())
          if (filter.contains(getConfigTopic((TopicIF)scopeIt.next())))
            filterMatches++;
        return filterMatches == filter.size();
      default: // SHOW_ALL_ASSOCIATION_SCOPES
        // No filter: Show all associations (regardless of scope).
        return true;
    }
  }
  
  public boolean isVisible(AssociationRoleIF role) {
    // A role is only visible if it is not part of an optimized
    // association, and the association object it is part of
    // is itself visible. The visibility of the roles is taken
    // care of in #assertNode()

    if (role.getAssociation().getRoles().size() == 2)
      return false;

    return isVisible(role.getAssociation());

  }

  /**
   * Finds the color for this association or topic type that is stored in the
   * topic map. Returns null if no color is stored in the topic map.
   */
  public Color lookupColor(TopicIF type, TopicIF occtype) {
    String value = getOccurrenceValue(type, occtype);
    if (value == null)  
      return null;
    return parseColor(value);
  }

  // --- Internal

  /**
   * Sets the color for this association or topic type in the topic map.
   */
  private void setColor(TopicIF type, TopicIF occtype, Color c) {
    setOccurenceValue(type, occtype, c.getRed() + " " + c.getGreen() + " "
        + c.getBlue());
  }

  /**
   * Constructs a Color instance from RGB values that are delimited within a
   * string.
   */
  private Color parseColor(String value) {
    try {
      StringTokenizer strtok = new StringTokenizer(value);
      int red = Integer.parseInt(strtok.nextToken());
      int green = Integer.parseInt(strtok.nextToken());
      int blue = Integer.parseInt(strtok.nextToken());

      return new Color(red, green, blue);
    } catch (Exception e) {
      throw new OntopiaRuntimeException(Messages.getString(
          "Viz.ColourParseError", value));
    }
  }

  // --- Color assigner

  /**
   * Helper class which can semi-randomly provide colors.
   */
  private class ColorAssigner {
    private int previous;
    private Color[] colors;

    private ColorAssigner() {
      previous = -1;

      colors = new Color[22];
      colors[0] = new Color(116, 10, 57);
      colors[1] = new Color(219, 1, 99);
      colors[2] = new Color(245, 157, 205);
      colors[3] = new Color(121, 9, 125);
      colors[4] = new Color(247, 21, 255);
      colors[5] = new Color(245, 179, 241);
      colors[6] = new Color(9, 20, 121);
      colors[7] = new Color(1, 34, 233);
      colors[8] = new Color(92, 114, 250);
      colors[9] = new Color(9, 125, 129);
      colors[10] = new Color(17, 235, 255);
      colors[11] = new Color(153, 245, 242);
      colors[12] = new Color(161, 245, 165);
      colors[12] = new Color(0, 238, 22);
      colors[13] = new Color(11, 85, 14);
      colors[14] = new Color(229, 255, 5);
      colors[15] = new Color(250, 248, 150);
      colors[16] = new Color(134, 55, 8);
      colors[17] = new Color(255, 71, 21);
      colors[18] = new Color(246, 164, 142);
      colors[19] = new Color(10, 106, 66);
      colors[20] = new Color(11, 22, 89);
      colors[21] = Color.darkGray;
    }

    public Color getNextColor() {
      if (previous < colors.length - 1)
        previous++;

      return colors[previous];
    }
  }

  /**
   * INTERNAL: Finds the start topic in the given topic map, and returns it.
   */
  public TopicIF getStartTopic(TopicMapIF graphtm) {
    return getTopicFromReference(graphtm, startTopic);
  }

  /**
   * INTERNAL: Finds the scoping topic in the given topic map, and returns it.
   */
  public TopicIF getScopingTopic(TopicMapIF graphtm) {
    return getTopicFromReference(graphtm, scopingTopic);
  }

  /**
   * INTERNAL: Finds the scoping topic in the given topic map, and returns it.
   */
  public TopicIF getScopingTopicHolder() {
    return scopingTopic;
  }

  private TopicIF getTopicFromReference(TopicMapIF graphtm, TopicIF reference) {
    // See #setTopicReference()
    // for full details how the start topic is referenced.

    String base = graphtm.getStore().getBaseAddress().getAddress();
    Iterator occurs = reference.getOccurrences().iterator();
    if (occurs.hasNext()) {
      OccurrenceIF occurrence = (OccurrenceIF) occurs.next();
      TopicIF occType = occurrence.getType();
      if (occType == null) {
        // This is just here for backwards compatability
        LocatorIF loc = occurrence.getLocator();
        TopicIF topic = (TopicIF) graphtm.getObjectByItemIdentifier(loc);
        // convert to new form
        if (topic != null)
          setStartTopic(topic);
        return topic;
      } else if (subjectIndicator.equals(occType)) {

        LocatorIF locator = occurrence.getLocator();
        if (locator == null) {
          String data = occurrence.getValue();
          try {
            locator = new URILocator(base + data);
          } catch (MalformedURLException e) {
            // Ignore any errors
          }
        }
        return graphtm.getTopicBySubjectIdentifier(locator);
      } else if (subject.equals(occType)) {
        LocatorIF locator = occurrence.getLocator();
        return graphtm.getTopicBySubjectLocator(locator);
      } else if (sourceLocator.equals(occType)) {
        LocatorIF locator = occurrence.getLocator();
        if (locator == null) {
          String data = occurrence.getValue();
          try {
            locator = new URILocator(base + data);
          } catch (MalformedURLException e) {
            // Ignore any errors
          }
        }
        return (TopicIF) graphtm.getObjectByItemIdentifier(locator);
      }
    }
    return null;
  }

  /**
   * Sets the start topic of the vizualization. We use the srclocator stored in
   * an occurrence as not all topics have a subjind.
   */
  public void setStartTopic(TopicIF extstart) {
    setTopicReference(startTopic, extstart, extstart.getTopicMap().getStore()
        .getBaseAddress().getAddress());
  }

  protected void setTopicReference(TopicIF topic, TopicIF extstart,
                                   String base) {
    clearTopic(topic); // removes the occurrences recording the srclocs

    for (Iterator iter = extstart.getSubjectIdentifiers().iterator(); iter
        .hasNext();) {
      LocatorIF locator = (LocatorIF) iter.next();
      if (locator.getAddress().startsWith(base))
        builder.makeOccurrence(topic, subjectIndicator, locator.getAddress()
            .substring(base.length()));
      else
        builder.makeOccurrence(topic, subjectIndicator, locator);
      return;
    }

    for (Iterator iter = extstart.getSubjectLocators().iterator(); iter
        .hasNext();) {
      LocatorIF locator = (LocatorIF) iter.next();
      if (locator.getAddress().startsWith(base))
        builder.makeOccurrence(topic, subject, locator.getAddress()
            .substring(base.length()));
      else
        builder.makeOccurrence(topic, subject, locator);
      return;
    }

    for (Iterator iter = extstart.getItemIdentifiers().iterator(); iter
        .hasNext();) {
      LocatorIF locator = (LocatorIF) iter.next();
      if (locator.getAddress().startsWith(base))
        builder.makeOccurrence(topic, sourceLocator, locator.getAddress()
            .substring(base.length()));
      else
        builder.makeOccurrence(topic, sourceLocator, locator);
      return;
    }
  }

  /** Clear the start topic if it was set. */
  public void clearTopic(TopicIF aTopic) {
    if (!aTopic.getOccurrences().isEmpty()) {
      Iterator it = new ArrayList(aTopic.getOccurrences()).iterator();
      while (it.hasNext()) {
        OccurrenceIF occur = (OccurrenceIF) it.next();
        occur.remove();
      }
    }
  }

  public TopicIF getTypeInstanceType() {
    return typeInstanceType;
  }

  public int getTypeVisibility(TopicIF selectedType) {
    String visibility = getOccurrenceValue(selectedType, typeVisible);
    if (visibility == null) {
      return selectedType == defaultAssociationType || 
             selectedType == defaultType ?
                 DEFAULT_FILTER_SELECTION :
                 VizTopicMapConfigurationManager.FILTER_DEFAULT;
      
    }
    if ("true".equals(visibility))
      return VizTopicMapConfigurationManager.FILTER_IN;
    if ("false".equals(visibility))
      return VizTopicMapConfigurationManager.FILTER_OUT;

    // The following should never happen, and is an error in the configuration.
    throw new OntopiaRuntimeException("Error in Vizigator configuration. " +
        "Filter setting should always be 'true' or 'false', but was'" + 
        visibility + "'");
  }

  public int getAssociationTypeShape(TopicIF selectedType) {
    return getOccurrenceValue(selectedType, assocTypeShape,
        getOccurrenceValue(defaultType, assocTypeShape, 
            TMRoleEdge.DEFAULT_SHAPE));
  }

  public int getAssociationTypeLineWeight(TopicIF selectedType) {
    return getOccurrenceValue(selectedType, typeLineWeight,
        getOccurrenceValue(defaultAssociationType, typeLineWeight,
            TMRoleEdge.DEFAULT_LINE_WEIGHT));
  }

  public int getTopicTypeShapePadding(TopicIF selectedType) {
    return getOccurrenceValue(selectedType, typeShapePadding,
        getOccurrenceValue(defaultType, typeShapePadding,
            TMTopicNode.DEFAULT_SHAPE_PADDING));
  }

  public String getTypeIconFilename(TopicIF type) {
    return getOccurrenceValue(type, typeIconFilename);
  }

  public Icon getTypeIcon(TopicIF type) {
    return lookupIcon(type, typeIconFilename, typeIcon);
  }

  public Font getTypeFont(TopicIF type) {
    return getTypeFont(type, getTypeFont(defaultType, getDefaultFont()));
  }

  public Font getAssociationTypeFont(TopicIF type) {
    return getTypeFont(type, getDefaultAssociationFont());
  }

  private Font getDefaultAssociationFont() {
    return defaultAssociationFont;
  }

  private Font getTypeFont(TopicIF type, Font df) {
    String result = getOccurrenceValue(type, typeFont);
    if (result == null) {
      if (type == defaultType)
        setTypeFont(defaultType, df);
      if (type == defaultAssociationType)
        setTypeFont(defaultAssociationType, df);
      return df;
    }
    return parseFont(result);
  }

  public void setTypeFont(TopicIF type, Font font) {
    setOccurenceValue(type, typeFont, serializeFont(font));
  }

  private String serializeFont(Font font) {
    // Fonts are serialized to the form:
    // "<family name>-<style>-<size>"

    StringBuilder buffer = new StringBuilder();
    buffer.append(font.getFamily());
    buffer.append("-");
    buffer.append(font.getStyle());
    buffer.append("-");
    buffer.append(font.getSize());

    String result = buffer.toString();

    fontCache.put(result, font);

    return result;
  }

  public Font parseFont(String fontString) {
    // Fonts are serialized to the form:
    // "<family name>-<style>-<size>"

    Font font = (Font) fontCache.get(fontString);
    if (font != null) {
      return font;
    }

    StringTokenizer tokenizer = new StringTokenizer(fontString, "-");

    return new Font(tokenizer.nextToken(), Integer.parseInt(tokenizer
        .nextToken()), Integer.parseInt(tokenizer.nextToken()));
  }

  private Icon lookupIcon(TopicIF type, TopicIF filenameTopic,
                          TopicIF iconTopic) {
    String filename = getOccurrenceValue(type, filenameTopic);
    if (filename == null)
      return null;

    ImageIcon icon = (ImageIcon) iconCache.get(filename);
    if (icon == null) {

      String base64 = getOccurrenceValue(type, iconTopic);
      if (base64 == null)
        return null;

      icon = new ImageIcon(Base64.decodeBase64(base64));
      iconCache.put(filename, icon);
    }
    return icon;
  }

  public void setTypeIconFilename(TopicIF type, String string) {
    if (string == null) {
      removeOccurence(type, typeIconFilename);
      removeOccurence(type, typeIcon);
    } else
      setIcon(type, string, typeIconFilename, typeIcon);

  }

  private void setIcon(TopicIF topictype, String string, TopicIF filenameTopic,
      TopicIF iconTopic) {
    setOccurenceValue(topictype, filenameTopic, string);

    ByteArrayOutputStream output = new ByteArrayOutputStream();

    try {
      FileInputStream file = new FileInputStream(string);
      IOUtils.copy(file, output);
      file.close();
      byte[] bytes = output.toByteArray();
      ImageIcon icon = new ImageIcon(bytes);
      iconCache.put(string, icon);
      output.reset();
      output.write(Base64.encodeBase64(bytes));
    } catch (IOException e) {
      // should never occur
      throw new OntopiaRuntimeException("INTERNAL ERROR", e);
    }

    try {
      setOccurenceValue(topictype, iconTopic, output.toString("ISO-8859-1"));
    } catch (UnsupportedEncodingException e1) {
      throw new OntopiaRuntimeException(e1);
    }
  }

  public boolean shouldDisplayRoleHoverHelp() {
    return getOccurrenceValue(generalTopic, showRoleHoverHelp, true);
  }

  public boolean isMotionKillerEnabled() {
    return getOccurrenceValue(generalTopic, enableMotionKiller, true);
  }

  public boolean shouldDisplayScopedAssociationNames() {
    return getOccurrenceValue(generalTopic, displayScopedAssociationNames,
        true);
  }

  public void shouldDisplayRoleHoverHelp(boolean value) {
    setOccurenceValue(generalTopic, showRoleHoverHelp, value);
  }

  public void setMotionKillerEnabled(boolean value) {
    setOccurenceValue(generalTopic, enableMotionKiller, value);
  }

  public void shouldDisplayScopedAssociationNames(boolean value) {
    setOccurenceValue(generalTopic, displayScopedAssociationNames, value);
  }

  public void setPanelBackgroundColour(Color aColor) {
    setColor(generalTopic, typeColor, aColor);
  }

  public Color getPanelBackgroundColour() {
    Color colour = lookupColor(generalTopic, typeColor);
    if (colour == null)
      colour = DEFAULT_PANEL_BACKGROUND_COLOUR;
    return colour;
  }

  public void setGeneralSingleClick(int anAction) {
    setOccurrenceValue(generalTopic, singleMouseClick, anAction);
  }

  public void setGeneralLocalityAlgorithm(int anAction) {
    setOccurrenceValue(generalTopic, localityAlgorithm, anAction);
  }

  public void setMotionKillerDelay(int seconds) {
    setOccurrenceValue(generalTopic, motionKillerDelay, seconds);
  }

  public void setMaxTopicNameLength(int length) {
    setOccurrenceValue(generalTopic, maxTopicNameLength, length);
  }

  public void setGeneralDoubleClick(int anAction) {
    setOccurrenceValue(generalTopic, doubleMouseClick, anAction);
  }

  public int getGeneralDoubleClick() {
    return getOccurrenceValue(generalTopic, doubleMouseClick,
        DEFAULT_DOUBLE_CLICK);
  }

  public int getGeneralSingleClick() {
    return getOccurrenceValue(generalTopic, singleMouseClick,
        DEFAULT_SINGLE_CLICK);
  }

  public int getGeneralLocalityAlgorithm() {
    return getOccurrenceValue(generalTopic, localityAlgorithm,
        DEFAULT_LOCALITY_ALGORITHM);
  }

  public int getGeneralMotionKillerDelay() {
    return getOccurrenceValue(generalTopic, motionKillerDelay,
        DEFAULT_KILLER_DELAY);
  }

  public int getMaxTopicNameLength() {
    return getOccurrenceValue(generalTopic, maxTopicNameLength,
        DEFAULT_MAX_TOPIC_NAME_LENGTH);
  }

  public void setTypeIncluded(TopicIF type) {
    setOccurenceValue(type, typeExcluded, false);
  }

  public void setTypeExcluded(TopicIF type) {
    setOccurenceValue(type, typeExcluded, true);
  }

  public boolean isTypeExcluded(TopicIF aType) {
    return getOccurrenceValue(aType, typeExcluded, false);
  }

  public void setScopingTopic(TopicIF scope) {
    if (scope == null)
      clearTopic(scopingTopic);
    else {
      setTopicReference(scopingTopic, scope, scope.getTopicMap().getStore()
          .getBaseAddress().getAddress());
    }
  }

  public void clearStartTopic() {
    clearTopic(startTopic);
  }

  protected TopicIF getSourceLocator() {
    return sourceLocator;
  }

  protected TopicIF getSubject() {
    return subject;
  }

  protected TopicIF getSubjectIndicator() {
    return subjectIndicator;
  }

  public void setInAssociationScopeFilter(TopicIF scope, boolean included) {
    setOccurenceValue(scope, scopeFilter, included);
  }

  public boolean isInAssociationScopeFilter(TopicIF scope) {
    return getOccurrenceValue(scope, scopeFilter, false);
  }
  
  public void setAssociationScopeFilterStrictness(int strictness) {
    setOccurrenceValue(scopeFilter, filterStrictness, strictness);
  }

  public int getAssociationScopeFilterStrictness() {
    int retVal = SHOW_ALL_ASSOCIATION_SCOPES;

    String occurrenceValue = getOccurrenceValue(scopeFilter, filterStrictness);
    try {
      if (occurrenceValue != null)
        retVal = Integer.parseInt(occurrenceValue);
    } catch (NumberFormatException e) {
    }
    
    return retVal;
  }
  
  /**
   * @return All topics that are used as scopes of associations and are used to
   * filter associations in Vizigator.
   */
  public Collection getAssociationScopeFilter() {
    // Get all topics that have an occurrence of type scopeFilter of value true.
    Collection retVal = new ArrayList();

    // Note: making new ArrayList() to avoid ConcurrentModificationException.
    Collection topics = new ArrayList(topicmap.getTopics());
    for (Iterator topicsIt = topics.iterator(); topicsIt.hasNext();) {
      TopicIF currentTopic = (TopicIF)topicsIt.next();
      
      if (isInAssociationScopeFilter(currentTopic))
        retVal.add(currentTopic);
    }
    return retVal;
  }
}
